/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

/*
 * ClockServer.java
 *
 * Created on 26-Mar-2011, 12:41:48
 */

package TimeRecording.TimeClockServer;

import TimeRecording.TimeClockServer.messages.ClockMessage;
import TimeRecording.TimeClockServer.dtos.TxRxBytes;
import TimeRecording.TimeClockServer.dtos.DatabaseTransaction;
import TimeRecording.TimeClockServer.enums.Component;
import TimeRecording.TimeClockServer.enums.ComponentStatus;
import java.awt.Color;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Formatter;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.WindowConstants;
import javax.swing.table.DefaultTableModel;

/**
 *
 * @author david
 */
public class ClockServer extends javax.swing.JFrame implements Runnable {

    // <editor-fold defaultstate="collapsed" desc="BadgeTableModel">
    private class TransactionTableModel extends DefaultTableModel {

        private final static int MAX_ENTRIES = 100;

        private final static int SYSTEM_TIME_COL = 0;
        private final static int CLOCK_TIME_COL = 1;
        private final static int BADGE_COL = 2;
        private final static int IN_OUT_COL = 3;

        private final  String[] HEADINGS = new String[] {
                "System Time", "Clock Time", "Badge ID", "In/Out"};

        private CyclicalLog<DatabaseTransaction> buffer =
                new CyclicalLog<DatabaseTransaction>(MAX_ENTRIES);

        @Override
        public int getColumnCount() {
            return HEADINGS.length;
        }

        @Override
        public int getRowCount() {
            if (buffer != null) {
                return buffer.size();
            } else {
                return 0;
            }
        }

        @Override
        public String getColumnName(int column) {
            return HEADINGS[column];
        }

        @Override
        public boolean isCellEditable(int row, int column) {
            return false;
        }

        @Override
        public Object getValueAt(int row, int column) {

            DatabaseTransaction transaction = buffer.getElement(row);
            if (transaction == null) {
                return null;
            }

            switch (column) {
                case SYSTEM_TIME_COL:
                    return transaction.getSystemDate();
                case CLOCK_TIME_COL:
                    return transaction.getClockDate();
                case BADGE_COL:
                    return transaction.getBadgeNo();
                case IN_OUT_COL:
                    return transaction.getClockedInOrOut();
                default:
                    return null;
            }
        }

        public void addTransaction(DatabaseTransaction transaction) {
            buffer.addElement(transaction);
            this.fireTableDataChanged();
        }
    } // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="TxRxTableModel">
    private class TxRxTableModel extends DefaultTableModel {

        private final static int MAX_ENTRIES = 400;

        private final static int SYSTEM_TIME_COL = 0;
        private final static int DIRECTION_COL = 1;
        private final static int MESSAGE_COL = 2;

        private final  String[] HEADINGS = new String[] {
                "System Time", "Direction", "Message Content"};

        private CyclicalLog<TxRxBytes> buffer =
                new CyclicalLog<TxRxBytes>(MAX_ENTRIES);

        @Override
        public int getColumnCount() {
            return HEADINGS.length;
        }

        @Override
        public int getRowCount() {
            if (buffer != null) {
                return buffer.size();
            } else {
                return 0;
            }
        }

        @Override
        public String getColumnName(int column) {
            return HEADINGS[column];
        }

        @Override
        public boolean isCellEditable(int row, int column) {
            return false;
        }

        @Override
        public Object getValueAt(int row, int column) {

            TxRxBytes transaction = buffer.getElement(row);
            if (transaction == null) {
                return null;
            }

            switch (column) {
                case SYSTEM_TIME_COL:
                    return transaction.getSystemDate();
                case DIRECTION_COL:
                    return transaction.getDirection();
                case MESSAGE_COL:
                    return MessageFormatter.bytesToText(transaction.getMessage());
                default:
                    return null;
            }
        }

        public void addTransaction(TxRxBytes transaction) {
            buffer.addElement(transaction);
            this.fireTableDataChanged();
        }
    } // </editor-fold>

    public final static Boolean USING_EOT_MARKER = true;

    private final static String SHUTDOWN_TITLE = "Shutdown Clock Server";
    private final static String SHUTDOWN_MESSAGE =
            "Are you sure you want to shutdown the application?";

    private final static String FORCE_SHUTDOWN_TITLE = "Shutdown Clock Server (Forced)";
    private final static String FORCE_SHUTDOWN_MESSAGE =
            "Are you sure you want to force shutdown of the application?\n" +
            "A normal shutdown is the preferred mechanism.";

    private final static String SET_TIME_TITLE = "Set Clock Terminal Time";
    private final static String SET_TIME_MESSAGE =
            "The time on the clock terminal will be re-synchronised with the\n" +
            "time on this computer; are you sure you wish to proceed?";

    private final static int TERMINAL_PORT = 2500;
    private final static int TERMINAL_TIMEOUT = 60 * 1000;
    private final static int MONITOR_TIMEOUT = 1 * 1000;
    private final static int SHUTDOWN_TIMEOUT = 3 * 1000;

    private DateCellRenderer dateRenderer = new DateCellRenderer();

    private TransactionTableModel badgeTableModel =
            new TransactionTableModel();

    private TxRxTableModel txRxTableModel =
            new TxRxTableModel();

    private RxBuffer rxBuffer = new RxBuffer();
    private TxBuffer txBuffer = new TxBuffer();

    public RxBuffer getRxBuffer() {
        return rxBuffer;
    }

    public TxBuffer getTxBuffer() {
        return txBuffer;
    }

    private boolean quit;

    // <editor-fold defaultstate="collapsed" desc="Default Constructor">
    public ClockServer() {

        initComponents();

        setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);

        // Tweaks to the badge table

        badgeTable.setModel(badgeTableModel);

        badgeTable.getColumnModel().getColumn(0).
                setCellRenderer(dateRenderer);
        badgeTable.getColumnModel().getColumn(1).
                setCellRenderer(dateRenderer);

        // Tweaks to the communications table

        txRxTable.setModel(txRxTableModel);

        txRxTable.getColumnModel().getColumn(0).
                setPreferredWidth(80);
        txRxTable.getColumnModel().getColumn(1).
                setPreferredWidth(50);
        txRxTable.getColumnModel().getColumn(2).
                setPreferredWidth(500);

        txRxTable.getColumnModel().getColumn(0).
                setCellRenderer(dateRenderer);
        txRxTable.getColumnModel().getColumn(1).
                setCellRenderer(dateRenderer);
    }
    // </editor-fold>

    /** This method is called from within the constructor to
     * initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is
     * always regenerated by the Form Editor.
     */
    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {
        java.awt.GridBagConstraints gridBagConstraints;

        clockStatusLabel = new javax.swing.JLabel();
        databaseStatusLabel = new javax.swing.JLabel();
        loggingPane = new javax.swing.JSplitPane();
        badgeScrollPane = new javax.swing.JScrollPane();
        badgeTable = new javax.swing.JTable();
        communicationsScrollPane = new javax.swing.JScrollPane();
        txRxTable = new javax.swing.JTable();
        buttonPanel = new javax.swing.JPanel();
        shutdown = new javax.swing.JButton();
        setTime = new javax.swing.JButton();
        history = new javax.swing.JButton();
        forceShutdown = new javax.swing.JButton();
        clockDriftLabel = new javax.swing.JLabel();

        setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE);
        setTitle("Time Clock Activity");
        addWindowListener(new java.awt.event.WindowAdapter() {
            public void windowClosing(java.awt.event.WindowEvent evt) {
                formWindowClosing(evt);
            }
        });
        getContentPane().setLayout(new java.awt.GridBagLayout());

        clockStatusLabel.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
        clockStatusLabel.setText("Clock Status");
        clockStatusLabel.setBorder(javax.swing.BorderFactory.createBevelBorder(javax.swing.border.BevelBorder.RAISED));
        clockStatusLabel.setMaximumSize(new java.awt.Dimension(34, 32));
        clockStatusLabel.setMinimumSize(new java.awt.Dimension(34, 32));
        clockStatusLabel.setOpaque(true);
        clockStatusLabel.setPreferredSize(new java.awt.Dimension(34, 32));
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = 0;
        gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
        gridBagConstraints.weightx = 0.5;
        gridBagConstraints.insets = new java.awt.Insets(4, 4, 4, 4);
        getContentPane().add(clockStatusLabel, gridBagConstraints);

        databaseStatusLabel.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
        databaseStatusLabel.setText("Database Status");
        databaseStatusLabel.setBorder(javax.swing.BorderFactory.createBevelBorder(javax.swing.border.BevelBorder.RAISED));
        databaseStatusLabel.setMaximumSize(new java.awt.Dimension(34, 32));
        databaseStatusLabel.setMinimumSize(new java.awt.Dimension(34, 32));
        databaseStatusLabel.setOpaque(true);
        databaseStatusLabel.setPreferredSize(new java.awt.Dimension(34, 32));
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 2;
        gridBagConstraints.gridy = 0;
        gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
        gridBagConstraints.weightx = 0.5;
        gridBagConstraints.insets = new java.awt.Insets(4, 4, 4, 4);
        getContentPane().add(databaseStatusLabel, gridBagConstraints);

        loggingPane.setDividerLocation(150);
        loggingPane.setOrientation(javax.swing.JSplitPane.VERTICAL_SPLIT);

        badgeScrollPane.setViewportView(badgeTable);

        loggingPane.setTopComponent(badgeScrollPane);

        communicationsScrollPane.setViewportView(txRxTable);

        loggingPane.setRightComponent(communicationsScrollPane);

        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = 1;
        gridBagConstraints.gridwidth = 3;
        gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
        gridBagConstraints.weightx = 1.0;
        gridBagConstraints.weighty = 1.0;
        getContentPane().add(loggingPane, gridBagConstraints);

        shutdown.setText("Shutdown");
        shutdown.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                shutdownActionPerformed(evt);
            }
        });
        buttonPanel.add(shutdown);

        setTime.setText("Set Clock Time");
        setTime.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                setTimeActionPerformed(evt);
            }
        });
        buttonPanel.add(setTime);

        history.setText("Force History Replay");
        history.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                historyActionPerformed(evt);
            }
        });
        buttonPanel.add(history);

        forceShutdown.setText("Force Shutdown");
        forceShutdown.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                forceShutdownActionPerformed(evt);
            }
        });
        buttonPanel.add(forceShutdown);

        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = 2;
        gridBagConstraints.gridwidth = 3;
        gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
        gridBagConstraints.insets = new java.awt.Insets(4, 4, 4, 4);
        getContentPane().add(buttonPanel, gridBagConstraints);

        clockDriftLabel.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
        clockDriftLabel.setBorder(javax.swing.BorderFactory.createBevelBorder(javax.swing.border.BevelBorder.RAISED));
        clockDriftLabel.setMaximumSize(new java.awt.Dimension(250, 18));
        clockDriftLabel.setMinimumSize(new java.awt.Dimension(250, 18));
        clockDriftLabel.setOpaque(true);
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 1;
        gridBagConstraints.gridy = 0;
        gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
        gridBagConstraints.insets = new java.awt.Insets(4, 4, 4, 4);
        getContentPane().add(clockDriftLabel, gridBagConstraints);

        java.awt.Dimension screenSize = java.awt.Toolkit.getDefaultToolkit().getScreenSize();
        setBounds((screenSize.width-834)/2, (screenSize.height-428)/2, 834, 428);
    }// </editor-fold>//GEN-END:initComponents

    private void formWindowClosing(java.awt.event.WindowEvent evt) {//GEN-FIRST:event_formWindowClosing
        shutdown();
    }//GEN-LAST:event_formWindowClosing

    private void shutdownActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_shutdownActionPerformed
        shutdown();
    }//GEN-LAST:event_shutdownActionPerformed

    public void shutdown() {

        int button = JOptionPane.showConfirmDialog(this,
                SHUTDOWN_MESSAGE, SHUTDOWN_TITLE,
                JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);

        if (button == JOptionPane.YES_OPTION) {
            quit = true;
        }
    }

    private void setTimeActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_setTimeActionPerformed

        int button = JOptionPane.showConfirmDialog(this,
                SET_TIME_MESSAGE, SET_TIME_TITLE,
                JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);

        if (button == JOptionPane.YES_OPTION) {
            txBuffer.recordTimeToBeTransmitted();
        }
    }//GEN-LAST:event_setTimeActionPerformed

    private void historyActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_historyActionPerformed
        txBuffer.addMessage(ClockMessage.REQUEST_HISTORY);
    }//GEN-LAST:event_historyActionPerformed

    private void forceShutdownActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_forceShutdownActionPerformed

        int button = JOptionPane.showConfirmDialog(this,
                FORCE_SHUTDOWN_MESSAGE, FORCE_SHUTDOWN_TITLE,
                JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);

        if (button == JOptionPane.YES_OPTION) {
            System.exit(0);
        }
    }//GEN-LAST:event_forceShutdownActionPerformed

    public void run() {
        setVisible(true);
    }

    @SuppressWarnings("SleepWhileHoldingLock")
    public void connectToTerminal() throws IOException {

        quit = false;
        while (!quit) {

            deferredStatusUpdate(Component.CLOCK, ComponentStatus.OFFLINE);
            deferredStatusUpdate(Component.DATABASE, ComponentStatus.OFFLINE);

            ServerSocket hostSocket = null;
            Socket terminalSocket = null;

            ClockSender tx = null;
            ClockReceiver rx = null;
            ClockLogic logic = null;

            try {
                hostSocket = new ServerSocket(TERMINAL_PORT);
                terminalSocket = hostSocket.accept();
                terminalSocket.setSoTimeout(TERMINAL_TIMEOUT);

                deferredStatusUpdate(Component.CLOCK, ComponentStatus.ONLINE);

                InputStream inStream = terminalSocket.getInputStream();
                OutputStream outStream = terminalSocket.getOutputStream();

                rx = new ClockReceiver(this, inStream);
                rx.start();

                tx = new ClockSender(this, outStream);
                tx.start();

                logic = new ClockLogic(this);
                logic.start();

                // Periodically check that the threads are all okay
                do {
                    Thread.sleep(MONITOR_TIMEOUT);
                } while (!quit && tx.isAlive() && rx.isAlive() && logic.isAlive());

                // At least one of the threads is "screwed" so
                // just kill everything off and we'll try again

                tx.interrupt();
                rx.interrupt();
                logic.interrupt();

                // Give the threads a couple of ticks to shutdown cleanly
                Thread.sleep(SHUTDOWN_TIMEOUT);

            } catch (Throwable e) {
                Logger.getLogger(this.getClass().getName()).log(Level.SEVERE, null, e);

            } finally {
                tx.interrupt();
                rx.interrupt();
                logic.interrupt();
                try { terminalSocket.close(); } catch (Throwable e) { }
                try { hostSocket.close(); } catch (Throwable e) { }
            }
        }
    }
    
    /**
    * @param args the command line arguments
    */
    public static void main(String args[]) throws IOException {
        
        ClockServer guiThread = new ClockServer();
        java.awt.EventQueue.invokeLater(guiThread);

        guiThread.connectToTerminal();

        System.exit(0);
    }

    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JScrollPane badgeScrollPane;
    private javax.swing.JTable badgeTable;
    private javax.swing.JPanel buttonPanel;
    private javax.swing.JLabel clockDriftLabel;
    private javax.swing.JLabel clockStatusLabel;
    private javax.swing.JScrollPane communicationsScrollPane;
    private javax.swing.JLabel databaseStatusLabel;
    private javax.swing.JButton forceShutdown;
    private javax.swing.JButton history;
    private javax.swing.JSplitPane loggingPane;
    private javax.swing.JButton setTime;
    private javax.swing.JButton shutdown;
    private javax.swing.JTable txRxTable;
    // End of variables declaration//GEN-END:variables

    // <editor-fold defaultstate="collapsed" desc="Synchronized GUI updates">

    private class DeferredLogTxRxBytes implements Runnable {

        private TxRxBytes bytes;

        public DeferredLogTxRxBytes(TxRxBytes bytes) {
            this.bytes = bytes;
        }

        public void run() {
            txRxTableModel.addTransaction(bytes);
        }
    }
    
    public void deferredLogTxRxBytes(TxRxBytes txRxBytes) {
        java.awt.EventQueue.invokeLater(new DeferredLogTxRxBytes(txRxBytes));
    }

    //==========================================================================

    private class DeferredLogTransaction implements Runnable {

        private DatabaseTransaction transaction;

        public DeferredLogTransaction(DatabaseTransaction transaction) {
            this.transaction = transaction;
        }

        public void run() {
            badgeTableModel.addTransaction(transaction);
        }
    }

    public void deferredLogTransaction(DatabaseTransaction transaction) {
        java.awt.EventQueue.invokeLater(new DeferredLogTransaction(transaction));
    }

    //==========================================================================

    private class DeferredDriftUpdate implements Runnable {

        private final static int SECONDS_IN_MINUTE = 60;
        private final static int SECONDS_IN_HOUR = 60 * 60;

        private final static String FORMAT =
                "CLOCK DRIFT: %1$s%2$02d:%3$02d:%4$02d";

        private long drift;

        public DeferredDriftUpdate(long drift) {
            this.drift = drift;
        }

        public void run() {

            String sign = (drift >= 0 ? "+" : "-");
            long seconds = Math.abs(drift);
            
            long hours = seconds / SECONDS_IN_HOUR;
            seconds -= (hours  * SECONDS_IN_HOUR);

            long minutes = seconds / SECONDS_IN_MINUTE;
            seconds -= (minutes  * SECONDS_IN_MINUTE);

            clockDriftLabel.setText(new Formatter().format(FORMAT,
                    sign, hours, minutes, seconds).toString());

            if (Math.abs(drift) < 60) {
                clockDriftLabel.setBackground(Color.green);

            } else if (Math.abs(drift) < 120) {
                clockDriftLabel.setBackground(Color.orange);

            } else {
                clockDriftLabel.setBackground(Color.red);
            }
        }
    }

    public void deferredDriftUpdate(long drift) {
        java.awt.EventQueue.invokeLater(new DeferredDriftUpdate(drift));
    }

    //==========================================================================

    private class DeferredStatusUpdate implements Runnable {

        private Component component;
        private ComponentStatus status;

        public DeferredStatusUpdate(Component component,
                ComponentStatus status) {
            this.component = component;
            this.status = status;
        }

        public void run() {

            JLabel statusLabel = null;
            String prefix = null;

            if (component == Component.CLOCK) {
                statusLabel = clockStatusLabel;
                prefix = "CLOCK TERMINAL ";

            } else if (component == Component.DATABASE) {
                statusLabel = databaseStatusLabel;
                prefix = "DATABASE ";
            }

            switch (status) {
                case OFFLINE:
                    statusLabel.setBackground(Color.RED);
                    statusLabel.setText(prefix + "OFFLINE");
                    break;
                case ONLINE:
                    statusLabel.setBackground(Color.GREEN);
                    statusLabel.setText(prefix + "ONLINE");
                    break;
            }
        }
    }

    public void deferredStatusUpdate(Component component,
            ComponentStatus status) {
        
        java.awt.EventQueue.invokeLater(new DeferredStatusUpdate(component, status));
    }
    // </editor-fold>
}
